package com.feedgraph;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.feedgraph.PlaceManager.Place;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.net.http.AndroidHttpClient;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;

/**
 * This needs to be re-worked to minimize the number of requests to
 * places.feedgraph.com How it _should_ work: (don't recall how it actually
 * _does_ work): - PlaceManager asks LocationManager for location changes - only
 * need to do this once, so can be in constructor - pass in frequency and
 * resolution info to pass along to LocationManager - When location changes
 * (should happen rarely more than once), ask feedgraph for nearby places - For
 * each nearby place, add a proximity alert to LocationManager - For each nearby
 * place, call PlacesListener.onNearbyPlace - When proximity alert is fired,
 * call PlacesListener.onEnteringPlace
 * 
 * @author ryanne
 */
public class PlaceManager extends BroadcastReceiver implements LocationListener {

	public static class Place implements Comparable <Place>, Parcelable {
		public static final String EXTRA = "com.feedgraph.PlaceManager.Places.EXTRA";
		public String name;
		public String id;
		public String icon;
		public double longitude, latitude;
		public float radius;

		public Place (JSONObject json) {
			name = json.optString ("Name");
			id = json.optString ("ID");
			icon = json.optString ("Icon");
			latitude = json.optDouble ("Latitude");
			longitude = json.optDouble ("Longitude");
			radius = (float) json.optDouble ("Radius", 100);
		}

		public Place (Parcel in) {
			name = in.readString ();
			id = in.readString ();
			icon = in.readString ();
			latitude = in.readDouble ();
			longitude = in.readDouble ();
			radius = in.readFloat ();
		}

		public Place (String name) {
			this.name = name;
		}

		@Override
		public int compareTo (Place another) {
			return id.compareTo (another.id);
		}

		public Location getLocation (String provider) {
			Location loc = new Location (provider);
			loc.setLatitude (latitude);
			loc.setLongitude (longitude);

			return loc;
		}

		public Uri getMapURI () {
			return Uri.parse ("geo:" + latitude + "," + longitude + "?z=15&q=" + Uri.encode (name));
		}

		public JSONObject toJSON () throws JSONException {

			JSONObject json = new JSONObject ();
			json.put ("Name", name);
			json.put ("ID", id);
			json.put ("Icon", icon);
			json.put ("Latitude", latitude);
			json.put ("Longitude", longitude);

			return json;
		}

		@Override
		public int describeContents () {
			return 0;
		}

		@Override
		public void writeToParcel (Parcel dest, int flags) {
			dest.writeString (name);
			dest.writeString (id);
			dest.writeString (icon);
			dest.writeDouble (longitude);
			dest.writeDouble (latitude);
			dest.writeFloat (radius);

		}

		public static final Parcelable.Creator <Place> CREATOR = new Parcelable.Creator <Place> () {
			public Place createFromParcel (Parcel in) {
				return new Place (in);
			}

			public Place [] newArray (int size) {
				return new Place [size];
			}
		};

	}

	public interface PlaceListener {

		/**
		 * indicates that a place is within a large radius from the device's
		 * location
		 * 
		 * @param place
		 */
		void onNearbyPlace (Place place);

		/**
		 * indicates that the device has moved within a tight radius of a place
		 * 
		 * @param place
		 */
		void onEnteringPlace (Place place);

		/**
		 * indicates that the device has moved away from a place
		 * 
		 * @param place
		 */
		void onLeavingPlace (Place place);
	}

	protected static final String PLACE_API_URI = "http://places.feedgraph.com/nearby";
	public static final String PROXIMITY_ALERT_INTENT = "com.feedgraph.ProximityAlert";
	protected Set <Place> places = new TreeSet <Place> ();
	protected List <PlaceListener> mListeners = new ArrayList <PlaceListener> ();
	protected int mRadius;
	private String mTypes;
	protected Context mContext;
	private LocationManager mLocationManager;
	private String mProvider;
	private Place mCurrentPlace;

	public PlaceManager (Context context, long minTime, float minDistance,
			int searchRadius, String types[]) {
		mContext = context;
		mTypes = TextUtils.join ("|", types);
		mRadius = searchRadius;

		mLocationManager = (LocationManager) context
				.getSystemService (Context.LOCATION_SERVICE);
		mProvider = mLocationManager.getBestProvider (new Criteria (), true);
		mLocationManager.requestLocationUpdates (mProvider, minTime, minDistance,
				this);

	}
	
	/** call to stop receiving notifications 
	 * 
	 */
	public void cancel() {
		mLocationManager.removeUpdates (this);
	}
	
	@Override
	protected void finalize() throws Throwable {
		super.finalize ();
		cancel();
	}
	
	/** If the device is not at a known Place, returns null 
	 * 
	 * @return
	 */
	public Place getCurrentPlace () {
		return mCurrentPlace;
	}

	/**
	 * Forget which places we've already seen. Re-notify if we get close to them
	 * again.
	 */
	public void reset () {
		places.clear ();
	}

	public void addListener (PlaceListener listener) {
		mListeners.add (listener);
	}

	@Override
	public void onLocationChanged (final Location location) {

		Log.d ("LocationManager", "GETing new place info");

		final List <Place> newPlaces = new LinkedList <Place> ();

		AsyncTask <Void, Void, Void> task = new AsyncTask <Void, Void, Void> () {

			@Override
			protected void onPostExecute (Void params) {

				Place closestPlace = null;
				float closestDistance = 500; // meters -- a pretty big place!

				for (Place p : newPlaces) {
					onNearbyPlace (p);
					addProximityAlert (p);

					float distance = p.getLocation (mProvider).distanceTo (location);

					if (distance < p.radius && distance < closestDistance) {
						closestDistance = distance;
						closestPlace = p;
						// already at the place
					}
				}

				if (closestPlace != null) {
					onEnteringPlace (closestPlace);
				}
			}

			@Override
			protected Void doInBackground (Void... params) {
				AndroidHttpClient client = AndroidHttpClient.newInstance ("");
				HttpGet req = new HttpGet (PLACE_API_URI + "?lat="
						+ location.getLatitude () + "&long=" + location.getLongitude ()
						+ "&radius=" + mRadius + "&types=" + Uri.encode (mTypes));
				
				Log.d("PlaceManager", req.getURI ().toString ());
				
				try {
					HttpResponse resp = client.execute (req);
					InputStream stream = resp.getEntity ().getContent ();
					String str = IOUtil.toString (stream);
					
					if (str.compareTo ("null") == 0) {
						Log.d ("PlaceManager", "nothing nearby");
						return null;
					}

					JSONArray json = new JSONArray (str);

					for (int i = 0; i < json.length (); i++) {
						Place place = new Place (json.getJSONObject (i));
						if (!places.contains (place)) {
							places.add (place);
							newPlaces.add (place);
						}
					}
				}
				catch (Exception e) {
					Log.d ("PlaceManager", "nearby", e);
				}
				finally {
					client.close ();
				}
				return null;
			}
		};

		task.execute ();
	}

	protected void onNearbyPlace (Place p) {
		for (PlaceListener listener : mListeners) {
			listener.onNearbyPlace (p);
		}
	}

	protected void addProximityAlert (Place p) {
		Intent intent = new Intent (PROXIMITY_ALERT_INTENT);
		intent.putExtra (Place.EXTRA, p);
		
		intent.setAction (p.id); // needed to differentiate this intent from others
		PendingIntent pending = PendingIntent.getBroadcast (mContext, 0, intent, 0);
		mLocationManager.addProximityAlert (p.latitude, p.longitude, p.radius, -1,
				pending);

	}

	@Override
	public void onProviderDisabled (String provider) {
		// TODO Auto-generated method stub

	}

	@Override
	public void onProviderEnabled (String provider) {
		// TODO Auto-generated method stub

	}

	@Override
	public void onStatusChanged (String provider, int status, Bundle extras) {
		// TODO Auto-generated method stub

	}

	/**
	 * called by LocationManager using PendingIntent for proximity alerts
	 */
	@Override
	public void onReceive (Context context, Intent intent) {


			Place p = (Place) intent.getParcelableExtra (Place.EXTRA);

			if (intent.getExtras ().getBoolean (
					LocationManager.KEY_PROXIMITY_ENTERING)) {
				onEnteringPlace (p);
			}
			else {
				onLeavingPlace (p);
			}
		
	}

	private void onLeavingPlace (Place p) {
		
		if (p.equals (mCurrentPlace)) {
			mCurrentPlace = null;
		}

		for (PlaceListener listener : mListeners) {
			listener.onLeavingPlace (p);
		}
	}

	private void onEnteringPlace (Place p) {
		
		mCurrentPlace = p;

		for (PlaceListener listener : mListeners) {
			listener.onEnteringPlace (p);
		}

	}

}
